03.5 精通自定义 View 之属性动画——AnimatorSet

返回自定义 View 目录

ValueAnimator 和 ObjectAnimator 都只能单单实现一个动画,那如果我们想要使用一个组合动画,就需要用到 AnimatorSet。

AnimatorSet 针对 ValueAnimator 和 ObjectAnimator 都是适用的,但一般而言,我们不会用到 ValueAnimator 的组合动画,所以我们这篇仅讲解 ObjectAnimator 下的组合动画实现。

在 AnimatorSet 中直接给为我们提供了两个方法 playSequentially 和 playTogether,playSequentially 表示所有动画依次播放,playTogether 表示所有动画一起开始。

3.5.1 playSequentially() 与 playTogether() 函数

1. playSequentially()

1
2
public void playSequentially(Animator... items);
public void playSequentially(List<Animator> items);

这里有两种声明,第一个是我们最常用的,它的参数是可变长参数,也就是说我们可以传进去任意多个 Animator 对象。这些对象的动画会逐个播放。第二个构造函数,是传进去一个 List<Animator> 的列表。原理一样,也是逐个去取 List 中的动画对象,然后逐个播放。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MainActivity extends AppCompatActivity {
private Button mButton;
private TextView mTv1, mTv2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
mButton = findViewById(R.id.btn);
mTv1 = findViewById(R.id.tv_1);
mTv2 = findViewById(R.id.tv_2);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doPlaySequentiallyAnimator();
}
});
}
private void doPlaySequentiallyAnimator() {
ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1,
"BackgroundColor", 0xffff00ff, 0xffffff00, 0xffff00ff);
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1,
"translationY", 0, 300, 0);
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2,
"translationY", 0, 400, 0);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playSequentially(tv1BgAnimator, tv1TranslateY, tv2TranslateY);
animatorSet.setDuration(1000);
animatorSet.start();
}
}

布局 act_main.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="start anim"/>
<TextView
android:id="@+id/tv_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="30dp"
android:background="#ff00ff"
android:padding="10dp"
android:text="TextView-2" />
<TextView
android:id="@+id/tv_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/tv_2"
android:layout_marginRight="30dp"
android:background="#ffff00"
android:padding="10dp"
android:text="TextView-1" />
</RelativeLayout>

2. playTogether()

1
2
public void playTogether(Animator... items);
public void playTogether(Collection<Animator> items);

将上例中的代码更改为:

1
2
// animatorSet.playSequentially(tv1BgAnimator, tv1TranslateY, tv2TranslateY);
animatorSet.playTogether(tv1BgAnimator, tv1TranslateY, tv2TranslateY);

即三个动画同时播放。

3、playSequentially 和 playTogether 函数的真正意义

想必大家都看到赛马,在赛马开始前,每个马都会被放在起点的小门后面,到点了,门打开,马开始一起往前跑。而假如我们把每匹马看做是一个动画,那我们的 playTogether 就相当于赛马场里每个赛道上门的意义(当比赛开始时,每个赛道上的门会打开,马就可以开始比赛了);也就是说,playTogether 只是一个时间点上的一起开始,对于开始后,各个动画怎么操作就是他们自己的事了,至于各个动画结不结束也是他们自已的事了。所以最恰当的描述就是门只负责打开,打开之后马咋跑,门也管不着,最后,马回不回来跟门也没啥关系。门的责任只是到点就打开而已。放在动画上,就是在激活动画之后,动画开始后的操作只是动画自己来负责。至于动画结不结束,也只有动画自己知道。

而 playSequentially 的意义就是当一匹马回来以后,再放另一匹。那如果上匹马永远没回来,那下一匹马也永远不会被放出来。放到动画上,就是把激活一个动画之后,动画之后的操作就是动画自己来负责了,这个动画结束之后,再激活下一个动画。如果上一个动画没有结束,那下一个动画就永远也不会被激活。

首先用 playTogether 来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1,
"BackgroundColor", 0xffff00ff, 0xffffff00, 0xffff00ff);
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1,
"translationY", 0, 300, 0);
tv1TranslateY.setStartDelay(1000);
tv1TranslateY.setRepeatCount(ValueAnimator.INFINITE);
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2,
"translationY", 0, 400, 0);
tv2TranslateY.setStartDelay(1000);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(tv1BgAnimator, tv1TranslateY, tv2TranslateY);
animatorSet.setDuration(1000);
animatorSet.start();

在这个例子中,我们将 tv1TranslateY 开始延迟 1000 毫秒开始,并设为无限循环。tv2TranslateY 设为开始延迟 1000 毫秒。而tv1BgAnimator 则是没有任何设置,所以是默认直接开始。我们来看效果图:

将上述例子做如下更改:

1
2
// animatorSet.playTogether(tv1BgAnimator, tv1TranslateY, tv2TranslateY);
animatorSet.playSequentially(tv1BgAnimator, tv1TranslateY, tv2TranslateY);

使用 playSequentially 来逐个播放这三个动画,首先是tv1BgAnimator,动画结束之后,激活 tv1TranslateY。不过由于设置了延时,故 1000 毫秒再开始,而且该动画会无限循环。无限循环也就是说它永远也不会结束。那么第三个动画 tv2TranslateY 也永远不会开始。效果图如下:

总结:

  • playTogether 和 playSequentially 在激活动画后,控件的动画情况与它们无关,他们只负责定时激活控件动画。
  • playSequentially 只有上一个控件做完动画以后,才会激活下一个控件的动画,如果上一控件的动画是无限循环,那下一个控件就别再指望能做动画了。

4. 实现无限循环动画

因为 AnimatorSet 中没有设置循环次数的函数,所以得为每个动画设置了无限循环,并且只能用 playTogether() 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1,
"BackgroundColor", 0xffff00ff, 0xffffff00, 0xffff00ff);
tv1BgAnimator.setRepeatCount(ValueAnimator.INFINITE);
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1,
"translationY", 0, 300, 0);
tv1TranslateY.setRepeatCount(ValueAnimator.INFINITE);
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2,
"translationY", 0, 400, 0);
tv2TranslateY.setRepeatCount(ValueAnimator.INFINITE);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(tv1BgAnimator, tv1TranslateY, tv2TranslateY);
animatorSet.setDuration(1000);
animatorSet.start();

3.5.2 AnimatorSet.Builder

1. 概述

playTogether 和 playSequentially,分别能实现一起开始动画和逐个开始动画。但并不是非常自由的组合动画,比如我们有三个动画 A、B、C,我们想先播放 C 然后同时播放 A 和 B。利用 playTogether 和 playSequentially 是没办法实现的,所以为了更方便的组合动画,谷歌的开发人员另外给我们提供一个类 AnimatorSet.Builder。

我们这里使用 AnimatorSet.Builder 实现两个控件一同开始动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1,
"BackgroundColor", 0xffff00ff, 0xffffff00, 0xffff00ff);
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1,
"translationY", 0, 300, 0);
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2,
"translationY", 0, 400, 0);
AnimatorSet animatorSet = new AnimatorSet();
AnimatorSet.Builder builder = animatorSet.play(tv1BgAnimator);
builder.with(tv1TranslateY).with(tv2TranslateY);
// animatorSet.playTogether(tv1BgAnimator, tv1TranslateY, tv2TranslateY);
animatorSet.setDuration(1000);
animatorSet.start();

2. AnimatorSet.Builder 的函数

从上面的代码中,我们可以看到 AnimatorSet.Builder 是通过 animatorSet.play(tv1BgAnimator) 生成的,这是生成AnimatorSet.Builder对象的唯一途径!

1
2
3
4
5
6
7
8
9
10
// 表示要播放哪个动画
public Builder play(Animator anim)
// 和前面动画一起执行
public Builder with(Animator anim)
// 执行前面的动画后才执行该动画
public Builder before(Animator anim)
// 执行先执行这个动画再执行前面动画
public Builder after(Animator anim)
// 延迟 n 毫秒之后执行动画
public Builder after(long delay)

play(Animator anim) 表示当前在播放哪个动画,另外的 with(Animator anim)、before(Animator anim)、after(Animator anim) 都是以 play 中的当前所播放的动画为基准的。

比如,当 play(playAnim) 与 before(beforeAnim) 共用,则表示在播放 beforeAnim 之前,先播放 playAnim 动画;同样,当 play(playAnim) 与 after(afterAnim) 共用时,则表示在在播放 afterAnim 动画之后,再播放 playAnim 动画。

每个函数的返回值都是 Builder 对象,于是可以使用串行方式使用它们:

1
animatorSet.play(tv1BgAnimator).with(tv1TranslateY).with(tv2TranslateY);

3.5.3 AnimatorSet 监听器

在 AnimatorSet 中也可以添加监听器,对应的监听器为:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static interface AnimatorListener {
// 当AnimatorSet开始时调用
void onAnimationStart(Animator animation);
// 当AnimatorSet结束时调用
void onAnimationEnd(Animator animation);
// 当AnimatorSet被取消时调用
void onAnimationCancel(Animator animation);
/**
* 当 AnimatorSet 重复时调用,由于 AnimatorSet 没有设置
* repeat 的函数,所以这个方法永远不会被调用。
*/
void onAnimationRepeat(Animator animation);
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class MainActivity extends AppCompatActivity {
private AnimatorSet mAnimatorSet;
private TextView mTv1, mTv2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
mTv1 = findViewById(R.id.tv_1);
mTv2 = findViewById(R.id.tv_2);
findViewById(R.id.start_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAnimatorSet = doPlayAnimatorSet();
}
});
findViewById(R.id.cancel_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mAnimatorSet != null) {
mAnimatorSet.cancel();
}
}
});
}
private AnimatorSet doPlayAnimatorSet() {
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1,
"translationY", 0, 400, 0);
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2,
"translationY", 0, 400, 0);
tv2TranslateY.setRepeatCount(ValueAnimator.INFINITE);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(tv1TranslateY).with(tv2TranslateY);
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.e("xian", "animator start");
}
@Override
public void onAnimationEnd(Animator animation) {
Log.e("xian", "animator end");
}
@Override
public void onAnimationCancel(Animator animation) {
Log.e("xian", "animator cancel");
}
@Override
public void onAnimationRepeat(Animator animation) {
Log.e("xian", "animator repeat");
}
});
animatorSet.setDuration(1000);
animatorSet.start();
return animatorSet;
}
}

日志输出如下:

总结一下 AnimatorSet 的监听:
AnimatorSet 的监听函数也只是用来监听 AnimatorSet 的状态的,与其中的动画无关。
AnimatorSet 中没有设置循环的函数,所以 AnimatorSet 监听器中永远无法运行到 onAnimationRepeat() 中。

3.5.4 常用函数

1. 概述

在 AnimatorSet 中还有几个函数:

1
2
3
4
5
6
// 设置单次动画时长
public AnimatorSet setDuration(long duration);
// 设置加速器
public void setInterpolator(TimeInterpolator interpolator)
// 设置ObjectAnimator动画目标控件
public void setTarget(Object target)

在 AnimatorSet 中设置以后,会覆盖单个 ObjectAnimator 中的设置;即如果 AnimatorSet 中没有设置,那么就以 ObjectAnimator 中的设置为准。如果 AnimatorSet 中设置以后,ObjectAnimator 中的设置就会无效。

下面我们简单举个例子来看下:

1
2
3
4
5
6
7
8
9
10
11
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
tv1TranslateY.setDuration(500000000);
tv1TranslateY.setInterpolator(new BounceInterpolator());
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
tv2TranslateY.setInterpolator(new AccelerateDecelerateInterpolator());
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(tv2TranslateY).with(tv1TranslateY);
animatorSet.setDuration(2000);
animatorSet.start();

在第这个例子中,我们通过 animatorSet.setDuration(2000); 设置为所有动画单词运动时长为 2000 毫秒,虽然我们给 tv1TranslateY 设置了单次动画时长为 tv1TranslateY.setDuration(500000000); 但由于 AnimatorSet 设置了 setDuration(2000) 这个参数以后,单个动画的时长设置将无效。所以每个动画的时长为 2000 毫秒。

但我们这里还分别给 tv1 和 tv2 设置了加速器,但并没有给 AnimatorSet 设置加速器,那么 tv1、tv2 将按各自加速器的表现形式做动画。同样,如果我们给 AnimatorSet 设置上了加速器,那么单个动画中所设置的加速器都将无效,以 AnimatorSet 中的加速器为准。

2. setTarget(Object target) 函数

1
2
// 设置 ObjectAnimator 动画目标控件
public void setTarget(Object target)

这个函数是用来设置目标控件的,也就是说,只要通过 AnimatorSet 的 setTartget 函数设置了目标控件,那么单个动画中的目标控件都以 AnimatorSet 设置的为准。

1
2
3
4
5
6
7
8
9
ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor",
0xffff00ff, 0xffffff00, 0xffff00ff);
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(tv1BgAnimator,tv2TranslateY);
animatorSet.setDuration(2000);
animatorSet.setTarget(mTv2);
animatorSet.start();

在这段代码中,我们给 tv1 设置了改变背景色,给 tv2 设置了上下移动。但由于我们通过 animatorSet.setTarget(mTv2); 将各个动画的目标控件设置为 mTv2,所以 tv1 将不会有任何动画,所有的动画都会发生在 tv2 上。

3. setStartDelay(long startDelay) 函数

1
2
// 设置延时开始动画时长
public void setStartDelay(long startDelay)

上面我们讲了,当 AnimatorSet 所拥有的函数与单个动画所拥有的函数冲突时,就以 AnimatorSet 设置为准。但唯一的例外就是 setStartDelay。

  • AnimatorSet 的延时是仅针对性的延长 AnimatorSet 激活时间的,对单个动画的延时设置没有影响。
  • AnimatorSet 真正激活延时 = AnimatorSet.startDelay + 第一个动画.startDelay
  • 在 AnimatorSet 激活之后,第一个动画绝对是会开始运行的,后面的动画则根据自己是否延时自行处理。

3.5.5 示例:路径动画

代码 MainActivity.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class MainActivity extends AppCompatActivity {
private Button btn1, btn2, btn3, btn4, btn5;
private boolean mIsMenuOpen = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
Button menu = findViewById(R.id.menu);
btn1 = findViewById(R.id.btn1);
btn2 = findViewById(R.id.btn2);
btn3 = findViewById(R.id.btn3);
btn4 = findViewById(R.id.btn4);
btn5 = findViewById(R.id.btn5);
menu.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mIsMenuOpen) {
closeMenu();
mIsMenuOpen = false;
} else {
openMenu();
mIsMenuOpen = true;
}
}
});
}
private void openMenu() {
doAnimateOpen(btn1, 0, 5, 600);
doAnimateOpen(btn2, 1, 5, 600);
doAnimateOpen(btn3, 2, 5, 600);
doAnimateOpen(btn4, 3, 5, 600);
doAnimateOpen(btn5, 4, 5, 600);
}
private void closeMenu() {
doAnimatColse(btn1, 0, 5, 600);
doAnimatColse(btn2, 1, 5, 600);
doAnimatColse(btn3, 2, 5, 600);
doAnimatColse(btn4, 3, 5, 600);
doAnimatColse(btn5, 4, 5, 600);
}
private void doAnimateOpen(View view, int index, int total, int radius) {
double degree = Math.toRadians(90) / (total - 1) * index;
int translationX = -(int) (Math.sin(degree) * radius);
int translationY = -(int) (Math.cos(degree) * radius);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(
ObjectAnimator.ofFloat(view, "translationX", 0, translationX),
ObjectAnimator.ofFloat(view, "translationY", 0, translationY),
ObjectAnimator.ofFloat(view, "scaleX", 0f, 1f),
ObjectAnimator.ofFloat(view, "scaleY", 0f, 1f),
ObjectAnimator.ofFloat(view, "alpha", 0f, 1f));
animatorSet.setDuration(500);
animatorSet.start();
}
private void doAnimatColse(View view, int index, int total, int radius) {
double degree = Math.toRadians(90) / (total - 1) * index;
int translationX = -(int) (Math.sin(degree) * radius);
int translationY = -(int) (Math.cos(degree) * radius);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(
ObjectAnimator.ofFloat(view, "translationX", translationX, 0),
ObjectAnimator.ofFloat(view, "translationY", translationY, 0),
ObjectAnimator.ofFloat(view, "scaleX", 1f, 0f),
ObjectAnimator.ofFloat(view, "scaleY", 1f, 0f),
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f));
animatorSet.setDuration(500);
animatorSet.start();
}
}

布局文件 act_main:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">
<Button
android:id="@+id/btn1"
style="@style/MenuItemStyle"
android:background="@drawable/circle1"/>
<Button
android:id="@+id/btn2"
style="@style/MenuItemStyle"
android:background="@drawable/circle2"/>
<Button
android:id="@+id/btn3"
style="@style/MenuItemStyle"
android:background="@drawable/circle3"/>
<Button
android:id="@+id/btn4"
style="@style/MenuItemStyle"
android:background="@drawable/circle4"/>
<Button
android:id="@+id/btn5"
style="@style/MenuItemStyle"
android:background="@drawable/circle5"/>
<Button
android:id="@+id/menu"
style="@style/MenuStyle"
android:background="@drawable/circle"/>
</FrameLayout>

引用资源 circle.xml:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/colorAccent"/>
<solid android:color="#983B90"/>
<solid android:color="#785B90"/>
<solid android:color="#587BA0"/>
<solid android:color="#389BF0"/>
<solid android:color="#18DB00"/>
</shape>

主题样式 style.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<resources>
...
<style name="MenuStyle" >
<item name="android:layout_width">50dp</item>
<item name="android:layout_height">50dp</item>
<item name="android:layout_gravity">right|bottom</item>
</style>
<style name="MenuItemStyle" >
<item name="android:layout_width">40dp</item>
<item name="android:layout_height">40dp</item>
<item name="android:layout_margin">5dp</item>
<item name="android:layout_gravity">right|bottom</item>
</style>
</resources>